{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "参考：\n",
    "1. [地址一](https://blog.csdn.net/lh0616/article/details/100067697?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task)\n",
    "2. [地址二](https://www.cnblogs.com/xleng/p/10948838.html)\n",
    "3. Gstreamer应用开发手册\n",
    "\n",
    "**gstreamer 官方网址**\n",
    "\n",
    "[https://gstreamer.freedesktop.org](https://gstreamer.freedesktop.org/documentation)\n",
    "\n",
    "# Gstreamer 是什么\n",
    "\n",
    "GStreamer 是一个创建流媒体应用程序的框架。其基本设计思想来自于俄勒冈(Oregon)研究生学院有关视频管道的创意, 同时也借鉴了 DirectShow 的设计思想。\n",
    "\n",
    "GStreamer 的程序开发框架使得编写任意类型的流媒体应用程序成为了可能。在编写处理音频、视频或者两者皆有的应用程序时, GStreamer 可以让你的工作变得简单。GStreamer 并不受限于音频和视频处理, 它能够处理任意类型的数据流。管道设计的方法对于实际应用的滤波器几乎没有负荷, 它甚至可\n",
    "以用来设计出对延时有很高要求的高端音频应用程序。\n",
    "\n",
    "GStreamer 最显著的用途是在构建一个播放器上。GStreamer 已经支持很多格式的文件了, 包括:MP3、Ogg/Vorbis、MPEG-1/2、AVI、Quicktime、 mod 等等。从这个角度看,GStreamer 更象是一个播放器。但是它主要的优点却是在于: 它的可插入组件能够很方便的接入到任意的管道当中。这个优点使得利用 GStreamer 编写一个万能的可编辑音视频应用程序成为可能。\n",
    "\n",
    "**GStreamer  框架是基于插件的**, 有些插件中提供了各种各样的多媒体数字信号编解码器,也有些提供了其他的功能。所有的插件都能够被链接到任意的已经定义了的数据流管道中。GStreamer 的管道能够被 GUI 编辑器编辑, 能够以 XML 文件来保存。这样的设计使得管道程序库的消耗变得非常少。\n",
    "\n",
    "GStreamer 核心库函数是一个处理插件、数据流和媒体操作的框架。GStreamer 核心库还提供了一个API, 这个API是开放给程序员使用的---当程序员需要使用其他的插件来编写他所需要的应用程序的\n",
    "时候可以使用它。\n",
    "\n",
    "下图是对基于Gstreamer框架的应用的简单分层：\n",
    "\n",
    "![gstreamer](https://md-picture-1254350681.cos.ap-beijing.myqcloud.com/1647252-20190530111815110-1437440871.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Media Applications\n",
    "\n",
    "最上面一层为应用，比如gstreamer自带的一些工具（gst-launch，gst-inspect等），以及基于gstreamer封装的库（gst-player，gst-rtsp-server，gst-editing-services等)根据不同场景实现的应用。\n",
    "\n",
    "## Core Framework\n",
    "中间一层为Core Framework，主要提供：\n",
    "\n",
    "- 上层应用所需接口\n",
    "- Plugin的框架\n",
    "- Pipline的框架\n",
    "- 数据在各个Element间的传输及处理机制\n",
    "- 多个媒体流（Streaming）间的同步（比如音视频同步）\n",
    "- 其他各种所需的工具库\n",
    "\n",
    "## Plugins\n",
    "\n",
    "最下层为各种插件，实现具体的数据处理及音视频输出，应用不需要关注插件的细节，会由Core Framework层负责插件的加载及管理。主要分类为：\n",
    "\n",
    "- Protocols：负责各种协议的处理，file，http，rtsp等。\n",
    "- Sources：负责数据源的处理，alsa，v4l2，tcp/udp等。\n",
    "- Formats：负责媒体容器的处理，avi，mp4，ogg等。\n",
    "- Codecs：负责媒体的编解码，mp3，vorbis等。\n",
    "- Filters：负责媒体流的处理，converters，mixers，effects等。\n",
    "- Sinks：负责媒体流输出到指定设备或目的地，alsa，xvideo，tcp/udp等。\n",
    "\n",
    "Gstreamer框架根据各个模块的成熟度以及所使用的开源协议，将core及plugins置于不同的源码包中：\n",
    "\n",
    "- gstreamer: 包含core framework及core elements。\n",
    "- gst-plugins-base: gstreamer应用所需的必要插件。\n",
    "- gst-plugins-good: 高质量的采用LGPL授权的插件。\n",
    "- gst-plugins-ugly: 高质量，但使用了GPL等其他授权方式的库的插件，比如使用GPL的x264，x265。\n",
    "- gst-plugins-bad: 质量有待提高的插件，成熟后可以移到good插件列表中。\n",
    "- gst-libav: 对libav封装，使其能在gstreamer框架中使用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Gstreamer 基本概念\n",
    "\n",
    "## 元件（element）\n",
    "\n",
    "    元件 (element) 是 GStreamer 中最重要的概念。你可以通过创建一系列的元件 (Elements), 并把它们连接起来, 从而让数据流在这个被连接的各个元件(Elements) 之间传输。每个元件 (Elements) 都有一个特殊的函数接口, 对于有些元件 (Elements) 的函数接口它们是用于能够读取文件的数据, 解码文件数据的。而有些元件 (Elements) 的函数接口只是输出相应的数据到具体的设备上 (例如, 声卡设备)。你可以将若干个元件(Elements) 连接在一起, 从而创建一个管道 (`pipeline`) 来完成一个特殊的任务, 例如, 媒体播放或者录音。GStreamer 已经默认安装了很多有用的元件 (Elements), 通过使用这些元件(Elements) 你能够构建一个具有多种功能的应用程序。当然, 如果你需要的话, 你可以自己编写一个新的元件 (Elements)。对于如何编写元件(Elements) 的话题在**GStreamer Plugin Writer's Guide** 中有详细的说明\n",
    "\n",
    "Elements 根据功能可以分为以下三种：\n",
    "\n",
    "*   Source elements（源元件）\n",
    "*   Sink elements     （接收元件）\n",
    "*   Filter elements    （过滤元件）\n",
    "\n",
    "### 源元件 (Source elements) \n",
    "\n",
    "为管道产生数据，比如从磁盘或者声卡读取数据，源元件不接收数据，仅产生数据。仅有一个源衬垫，形象化的源元件如下图所示\n",
    "\n",
    "![](https://img-blog.csdnimg.cn/20190825212256567.png)\n",
    "\n",
    "### 过滤元件（Filter elements）\n",
    "\n",
    "包括过滤器 (filters)、转换器(convertors)、分流器(demuxers)、整流器(muxers) 以及编解码器 (codecs) 等等，同时拥有输入和输出衬垫，形象化过滤元件如下图所示\n",
    "\n",
    "![](https://img-blog.csdnimg.cn/20190825212447316.png)\n",
    "\n",
    "### 接收元件\n",
    "\n",
    "是媒体管道的末端，它接收数据但不产生任何数据。仅有一个接收衬垫，形象化接收元件如下图所示\n",
    "\n",
    "![](https://img-blog.csdnimg.cn/20190825212745385.png)\n",
    "\n",
    "### 元件状态\n",
    "\n",
    "包括以下几种：\n",
    "\n",
    "- GST_STATE_NULL: 默认状态\n",
    "\n",
    "- GST_STATE_READY: 准备状态\n",
    "\n",
    "- GST_STATE_PAUSED: 暂停状态\n",
    "\n",
    "- GST_STATE_PLAYING: 运行状态\n",
    "\n",
    "NULL和READY状态下，element不对数据做任何处理，PLAYING状态对数据进行处理，PAUSE状态介于两者之间，对数据进行preroll。应用程序通过函数调用控制pipeline在不同状态之间进行转换。\n",
    "\n",
    "element的状态变换不能跳过中间状态，比如不能从READY状态直接变换到PLAYING状态，必须经过中间的PAUSE状态。\n",
    "\n",
    "**element的状态转换成PAUSE会激活element的pad。首先是source pad被激活，然后是sink pad**。pad被激活后会调用activate函数，有一些pad会启动一个Task。\n",
    "\n",
    "**PAUSE状态下，pipeline会进行数据的preroll，目的是为后续的PLAYING状态准备好数据，使得PLAYING启动的速度更快。一些element需接收到足够的数据才能完成向PAUSE状态的转变，sink pad只有在接收到第一个数据才能实现向PAUSE的状态转变。**\n",
    "\n",
    "通常情况下，element的状态转变需要协调一致。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 衬垫 (Pads)\n",
    "\n",
    "Pad是一个element的输入/输出接口，分为src pad（生产数据）和sink pad（消费数据）两种。\n",
    "\n",
    "衬垫 (Pads) 是元件对外的接口。一个衬垫(Pads)可以被看作是一个元件(element)插座或者端口,元件(element)之间的链接就是依靠着衬垫(Pads)，**数据流从一个元件的源衬垫 (source pad) 到另一个元件的接收衬垫 (sink pad)。**  \n",
    " \n",
    "衬垫(Pads)有处理特殊数据的能力:一个衬垫(Pads)能够限制数据流类型的通过。链接成功的条件是：**只有在两个衬垫(Pads)允许通过的数据类型一致的时候才被建立**。两个element必须通过pad才能连接起来，pad拥有当前element能处理数据类型的能力（capabilities），会在连接时通过比较src pad和sink pad中所支持的能力，来选择最恰当的数据类型用于传输，如果element不支持，程序会直接退出。在element通过pad连接成功后，数据会从上一个element的src pad传到下一个element的sink pad然后进行处理。\n",
    "\n",
    "下面的这个比喻可能对你理解衬垫(Pads)有所帮助。一个衬垫(Pads)很象一个物理设备上的插头。例如一个家庭影院系统。一个家庭影院系统由一个功放(amplifier),一个 DVD 机,还有一个无声的视频投影组成。 我们需要连接 DVD 机到功放(amplifier),因为两个设备都有音频插口;我们还需要连接投影机到 DVD 机上,因为 两个设备都有视频处理插口。但我们很难将投影机与功放(amplifier)连接起来,因为他们之间处理的是不同的 插口。GStreamer 衬垫(Pads)的作用跟家庭影院系统中的插口是一样的。\n",
    "\n",
    "当element支持多种数据处理能力时，我们可以通过Cap来指定数据类型.\n",
    "例如，下面的命令通过Cap指定了视频的宽高，videotestsrc会根据指定的宽高产生相应数据：\n",
    "\n",
    "```sh\n",
    "gst-launch-1.0 videotestsrc ! \"video/x-raw,width=1280,height=720\" ! autovideosink\n",
    "```\n",
    "\n",
    "对于大部分情况,所有的数据流都是在链接好的元素之间流动。数据向元件(element)以外流出可以通过一个或者多个 source 衬垫(Pads),元件(element)接受数据是通过一个或者多个 sink 衬垫(Pads)来完成的。\n",
    "**Source 元件(element)和 sink 元件(element)分别有且仅有一个 sink 衬垫(Pads)或者 source 衬垫(Pads)。**\n",
    "\n",
    "Pads 的分类：\n",
    "\n",
    "- 永久型（always）\n",
    "- 随机型（sometimes）\n",
    "- 请求型 （on request）\n",
    "\n",
    "大部分情况下，所有在 GStreame r 中流经的数据都遵循一个原则：**数据从 element 的一个或多个源衬垫流出，从一个或多个sink 衬垫流入。源和 sink 元件分别只有源和 sink 衬垫。**\n",
    "\n",
    "**数据在这里代表的是缓冲区(buffers)([GstBuffer对象描述了数据的缓冲区(buffers)的信息](https://gstreamer.freedesktop.org/documentation/gstreamer/gstbuffer.html?gi-language=c#GstBuffer)和事件(events) ([GstEvent 对象描述了数据的事件(events)信息](https://gstreamer.freedesktop.org/documentation/gstreamer/gstevent.html?gi-language=c)。)**\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ❤️箱柜（bin） 和管道（Pipeline）\n",
    "\n",
    "箱柜 (Bins) 是一个可以装载元件 (element) 的容器。管道 (pipelines) 是箱柜 (Bins) 的一个特殊的子类型, 管道 (pipelines) 可以操作包含在它自身内部的所有元件 (element)。因为箱柜(Bins) 本身又是组件 (element) 的子集, 所以你能够象操作普通组件 (element) 一样的操作一个箱柜 (Bins), 通过这种方法可以降低你的应用程序的复杂度。\n",
    "\n",
    "**你可以改变一个箱柜(Bins) 的状态来改变箱柜 (Bins) 内部所有组件 (element) 的状态**。箱柜 (Bins) 可以发送总线消息 (bus messages) 给它的子集组件(element)(这些消息包括: 错误消息(error messages), 标签消息(tag messages),EOS 消息(EOS messages))。如果没有bin，我们需要依次操作我们所使用的element。通过bin降低了应用的复杂度。\n",
    "\n",
    "形象化箱柜如下图所示\n",
    "\n",
    "![](https://md-picture-1254350681.cos.ap-beijing.myqcloud.com/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMjg5MzAyOC0zNmNjNmYyY2ViNjI3Y2U1LnBuZz9pbWFnZU1vZ3IyL2F1dG8tb3JpZW50Lw)\n",
    "\n",
    "<br>\n",
    "\n",
    "管道(pipeline)是高级的箱柜(Bins)。**当你设定管道的暂停或者播放状态的时候,数据流将开始流动,并且媒体数据处理也开始处理。一旦开始,管道将在一个单独的线程中运行,直到被停止或者数据流播放完毕。**\n",
    "\n",
    "## 能力集（Cap）\n",
    "\n",
    "Caps 描述了能够通过衬垫或当前通过衬垫的数据流格式。一个衬垫能够有多个功能。功能 (Caps) 可以用一个包含一个或多个 GstStructures 的数组来表示。每个 GstStructures 由一个名字字符串 (比如说 \"width\") 和相应的值 (类型可能为 G_TYPE_INT 或 GST_TYPE_INT_RANGE) 构成。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Gstreamer数据消息交互\n",
    "\n",
    "## 数据流\n",
    "\n",
    "**Gstreamer支持两种类型的数据流，分别是push模式和pull模式**。在push模式下，upstream的element通过调用downstream的sink pads的函数实现数据的传送。在pull模式下，downstream的element通过调用upstream的source pads的函数实现对数据的请求。\n",
    "\n",
    "**push模式是常用的模式，pull模式一般用于demuxer或者低延迟的音频应用等。**\n",
    "\n",
    "## 通信对象类型\n",
    "\n",
    "在pipeline运行的过程中，各个element以及应用之间不可避免的需要进行数据消息的传输，gstreamer提供了bus系统以及多种数据类型（Buffers、Events、Messages，Queries）来达到此目的:\n",
    "\n",
    "![Gstreamer数据消息交互](https://md-picture-1254350681.cos.ap-beijing.myqcloud.com/20201126142625.png)\n",
    "\n",
    "### Bus\n",
    "\n",
    "Bus是gstreamer内部用于将消息从内部不同的streaming线程，传递到bus线程，再由bus所在线程将消息发送到应用程序。应用程序只需要向bus注册消息处理函数，即可接收到pipline中各element所发出的消息，使用bus后，应用程序就不用关心消息是从哪一个线程发出的，避免了处理多个线程同时发出消息的复杂性。\n",
    "\n",
    "### Buffers\n",
    "\n",
    "用于从sources到sinks的媒体数据传输。\n",
    "\n",
    "缓冲区包含了你创建的管道里的数据流。**通常一个源组件会创建一个新的缓冲区，同时组件还将会把缓冲区的数据传递给下一个组件。**当使用 GStreamer 底层构造来创建一个媒体管道的时候，你不需要自己来处理缓冲区, 组件将会为你处理这些缓冲区。\n",
    "\n",
    "在pads之间传送的数据封装在Buffer里，一个缓冲区主要由以下几个组成：\n",
    "\n",
    "- 指向某块内存的指针\n",
    "- 内存的大小\n",
    "- 缓冲区的时间戳\n",
    "- 一些metadata，包括\n",
    "    - Timestamp\n",
    "    - Offset\n",
    "    - Duration\n",
    "    - media type\n",
    "    - 其它\n",
    "\n",
    "- 一个引用计数，指出了缓冲区所使用的组件数。没有组件可引用的时候，这个引用将用于销毁缓冲区。\n",
    "\n",
    "在 push 模式下，element通过调用`gst_pad_push()`函数把buffer传送给对应的pad。在 pull 模式下，element通过调用`gst_pad_pull_range()`函数把数据 pull 过来。\n",
    "element在push buffer之前需要确认对应的element具备处理buffer中的数据类型的能力。在传输之前首先查询对应的element能够处理的格式的种类，并从中选择合适的格式，通过`gst_buffer_set_caps()`函数对buffer进行设置，然后才传送数据。\n",
    "\n",
    "收到一个buffer后，element要首先对buffer进行检查以确认是否能够处理。\n",
    "\n",
    "可以调用`gst_buffer_new()`函数创建一个新的buffer，也可以调用`gst_pad_alloc_buffer()`函数申请一个可用的buffer。采用第二种方法接收数据的buffer可以设定接收其它类型的数据，这是通过对buffer的caps进行设定来实现的。\n",
    "\n",
    "**选择媒体类型并对buffer进行设定的处理过程叫做caps negotianation。**\n",
    "\n",
    "[GstBuffer对象描述了数据的缓冲区(buffers)的信息](https://gstreamer.freedesktop.org/documentation/gstreamer/gstbuffer.html?gi-language=c#GstBuffer)\n",
    "\n",
    "### Events\n",
    "\n",
    "用于element之间或者应用到element之间的信息传递，比如播放时的seek操作是通过event实现的。\n",
    "\n",
    "事件是一系列控制粒子，随着缓冲区被发送到管道的上游和下游。下游事件通知流状态相同的组件，可能的事件包括中断，flush，流的终止信号等等。**在应用程序与组件之间的交互以及事件与事件之间的交互中, 上游事件被用于改变管道中数据流的状态，如查找。**对于应用程序来说，上游事件非常重要，下游事件则是为了说明获取更加完善的数据概念上的图像。\n",
    "\n",
    "[GstEvent 对象描述了数据的事件(events)信息](https://gstreamer.freedesktop.org/documentation/gstreamer/gstevent.html?gi-language=c)\n",
    "\n",
    "### Messages\n",
    "\n",
    "是由element发出的消息，通过bus，以异步的方式被应用程序处理。通常用于传递errors, tags, state changes, buffering state, redirects等消息。消息处理是线程安全的。由于大部分消息是通过异步方式处理，所以会在应用程序里存在一点延迟，如果要及时的相应消息，需要在streaming线程捕获处理。\n",
    "\n",
    "### Queries\n",
    "\n",
    "用于应用程序向gstreamer查询总时间，当前时间，文件大小等信息。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# pipeline是啥？\n",
    "pipeline是GStreamer设计的核心思想,在说pipeline之前先弄一个meidaplayer的模型,meidaplayer是GStreamer比较重要的应用之一。不管是VLC、mplayer这些开源的多媒体播放器，还是其他商用的。meidaplayer都能抽象成以下的模型：\n",
    "\n",
    "![pic](../../assets/gst-pipeline.png)\n",
    "\n",
    "上面这个图就是个典型的meida播放器的模型：\n",
    "\n",
    "source: 数据来源，可能是file、http、rtp等.\n",
    "\n",
    "demuxer: 分流器，负责把容器里的音视频数据剥离出来，然后分别送给audio\\video  decoder.\n",
    "\n",
    "decoder: 解码,然后把解完后的数据(yuv、pcm)送给audio\\video output输出.\n",
    "\n",
    "output: 负责将decoder过来的数据呈现出来.\n",
    "\n",
    "如果把数据想象成流水的话，每个模块的功能虽然不同,但是他们大致抽象的功能都是接收上个模块过来的数据,然后加工把加工后的数据送到下一个模块。\n",
    "\n",
    "把上述这些模块通过某种方式连接起来,就形成了一个流水线(pipeline),这个流水线就是一个media播放器。\n",
    "\n",
    "GStreamer 就是把每个模块都看做是一个Element,然后构建了连接和操作这些Element的方法,用户可以通过自己的需求把不同的Elements 排列组合，形成一个又一个的不同的pipeline。\n",
    "\n",
    "你可以形成具有mediaplayer功能 的pipeline 、VOIP功能的pipeline。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# gstreamer tools\n",
    "\n",
    "Gstreamer自带了gst-inspect-1.0和gst-launch-1.0等其他命令行工具，我们可以使用这些工具完成常见的处理任务。\n",
    "\n",
    "## gst-inspect-1.0\n",
    "\n",
    "查看gstreamer的plugin、element的信息。直接将plugin/element的类型作为参数，会列出其详细信息。如果不跟任何参数，会列出当前系统gstreamer所能查找到的所有插件。\n",
    "\n",
    ">$ gst-inspect-1.0 playbin\n",
    "\n",
    "## gst-launch-1.0\n",
    "\n",
    "用于创建及执行一个Pipline，因此通常使用gst-launch先验证相关功能，然后再编写相应应用。一个pipeline的多个element之间通过 “!\" 分隔，同时可以设置element及Cap的属性。例如：  \n",
    "播放音视频\n",
    "\n",
    ">gst-launch-1.0 playbin file:///home/root/test.mp4\n",
    "\n",
    "转码\n",
    "\n",
    ">gst-launch-1.0 filesrc location=/videos/sintel_trailer-480p.ogv ! decodebin name=decode ! \\\n",
    "               videoscale ! \"video/x-raw,width=320,height=240\" ! x264enc ! queue ! \\\n",
    "               mp4mux name=mux ! filesink location=320x240.mp4 decode. ! audioconvert ! \\\n",
    "               avenc_aac ! queue ! mux.\n",
    "               \n",
    "Streaming\n",
    "\n",
    "#Server\n",
    ">gst-launch-1.0 -v videotestsrc ! \"video/x-raw,framerate=30/1\" ! x264enc key-int-max=30 ! rtph264pay ! udpsink host=127.0.0.1 port=1234\n",
    "\n",
    "#Client\n",
    ">gst-launch-1.0 udpsrc port=1234 ! \"application/x-rtp, payload=96\" ! rtph264depay ! decodebin ! autovideosink sync=false\n",
    " "
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "218.625px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
